﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Collections.ObjectModel;
using System.Globalization;

using Microsoft.Crm.Tools.Email.Providers;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Crm;
using System.Net.Mail;



namespace CustomOutgoingProvider
{
	/// <summary>
	/// Provider to retrieve emails on first come first basis, instead of binding to users/queues.
	/// </summary>
	public class CustomSmtpProvider : SmtpPollingSendEmailProvider
	{
		/// <summary>
		/// Ctor
		/// </summary>
		/// <param name="providerConfiguration"></param>
		/// <param name="shutdownEvent"></param>
		public CustomSmtpProvider(ProviderConfiguration providerConfiguration, ManualResetEvent shutdownEvent) :
			base(providerConfiguration, shutdownEvent)
		{
			_delayedEmailsRetrieved = DateTime.MinValue;
		}

		/// <summary>
		/// Overridden implementation to return SmtpClient object. This is primarily done to dispose SmtpClient object at the end of provider execution.
		/// </summary>
		/// <returns></returns>
		protected override SmtpClient RetrieveConfiguredSmtpClient()
		{
			return this.ConfiguredSmtpClient;
		}

		//Lock object to synchronize email retrieval across multiple threads.
		private static object lockObj = new object();
		/// <summary>
		/// Provider calls this method for a new email to be sent. Handles retrieval of emails from CRM to send.
		/// </summary>
		/// <returns></returns>
		protected override Microsoft.Xrm.Sdk.Entity RetrieveNextMessageInternal()
		{
			Entity retEmail = null;
			if (null != this._emails)
			{
				retEmail = this._emails.RetrieveNext();
				if (null != retEmail)
				{
					this.HasAttachments = this._emails.CurrentHasAttachment;
					return retEmail;
				}
				else
				{
					this._emails = null;
				}
			}

			_emails = new BatchEmails();

			//Let's serialize calls to retrieve emails as it will help avoid duplicate emails getting picked.
			lock (lockObj)
			{
				GetPendingEmails();
			}

			retEmail = this._emails.RetrieveNext();
			if (null != this._emails)
			{
				if (null != retEmail)
				{
					this.HasAttachments = this._emails.CurrentHasAttachment;
					return retEmail;
				}
				else
				{
					this._emails = null;
				}
			}

			//So, there are no more emails to send, let's dispose SmtpClient object.
			if (null != this.configuredSmtpClient)
			{
				this.ConfiguredSmtpClient.Dispose();
				this.configuredSmtpClient = null;
			}
			return null;
		}

		/// <summary>
		/// Retrieve emails in following order: Sending State emails, Pending state emails, New Emails.
		/// </summary>
		private void GetPendingEmails()
		{
			//Retrieve Sending state emails if any.
			//Retrieve Pending state emails if any.
			//Let's not call it very frequently.
			if (DateTime.UtcNow.Subtract(_delayedEmailsRetrieved).TotalMinutes > 5)
			{
				GetSendingStateEmails();
				GetPendingSendStateEmails();
				_delayedEmailsRetrieved = DateTime.UtcNow;
			}

			//Retrieve New emails.
			this.ServiceLogger.Log(System.Diagnostics.EventLogEntryType.Information, LogLevel.Details, "Retrieving new Emails", 31431);
			GetNewEmails();
		}

		private void GetNewEmails()
		{
			QueryExpression query = new QueryExpression("email");
			query.PageInfo.Count = 25;
			query.PageInfo.PageNumber = 1;

			query.ColumnSet = new ColumnSet("from", "to", "cc", "bcc", "subject", "description", "prioritycode", "activityid", "modifiedon");
			FilterExpression filter = new FilterExpression();
			filter.AddCondition("deliveryattempts", ConditionOperator.Equal, 0);
			filter.AddCondition("statuscode", ConditionOperator.Equal, 6); //6 is for Pending Send
			query.Criteria = filter;

			BackgroundSendEmailRequest request = new BackgroundSendEmailRequest();
			request.Query = query;
			BackgroundSendEmailResponse response = (BackgroundSendEmailResponse)this.GetCrmService().Execute(request);

			if (null != response)
			{
				_emails.AddRange(response.EntityCollection, response.HasAttachments);
			}
		}

		private void GetPendingSendStateEmails()
		{
			QueryExpression query = new QueryExpression("email");
			query.PageInfo.Count = 25;
			query.PageInfo.PageNumber = 1;

			query.ColumnSet = new ColumnSet("from", "to", "cc", "bcc", "subject", "description", "prioritycode", "activityid", "modifiedon");
			FilterExpression filter = new FilterExpression();
			filter.AddCondition("deliveryattempts", ConditionOperator.GreaterThan, 0);
			filter.AddCondition("statuscode", ConditionOperator.Equal, 6); //6 is for Pending Send
			filter.AddCondition("modifiedon", ConditionOperator.LessThan, GetPendingDelay().ToString("u", CultureInfo.InvariantCulture));
			query.Criteria = filter;

			BackgroundSendEmailRequest request = new BackgroundSendEmailRequest();
			request.Query = query;
			BackgroundSendEmailResponse response = (BackgroundSendEmailResponse)this.GetCrmService().Execute(request);

			if (null != response)
			{
				_emails.AddRange(response.EntityCollection, response.HasAttachments);
			}
		}

		private void GetSendingStateEmails()
		{
			QueryExpression query = new QueryExpression("email");
			query.PageInfo.Count = 25;
			query.PageInfo.PageNumber = 1;

			query.ColumnSet = new ColumnSet("from", "to", "cc", "bcc", "subject", "description", "prioritycode", "activityid", "modifiedon");
			FilterExpression filter = new FilterExpression();
			filter.AddCondition("deliveryattempts", ConditionOperator.GreaterThan, 0);
			filter.AddCondition("statuscode", ConditionOperator.Equal, 7); //7 is for Sending state
			filter.AddCondition("modifiedon", ConditionOperator.LessThan, GetSendingDelay().ToString("u", CultureInfo.InvariantCulture));

			query.Criteria = filter;

			BackgroundSendEmailRequest request = new BackgroundSendEmailRequest();
			request.Query = query;
			BackgroundSendEmailResponse response = (BackgroundSendEmailResponse)this.GetCrmService().Execute(request);

			if (null != response)
			{
				_emails.AddRange(response.EntityCollection, response.HasAttachments);
			}
		}

		private DateTime GetPendingDelay()
		{
			const int pendingDelayDefault = 20 * 60 * 1000;

			int pendingDelay = this.ProviderConfiguration.ConfigNodeReader.GetCheckedIntValue("PendingStatusDelay", pendingDelayDefault, 0, Int32.MaxValue, true);
			if (pendingDelay < pendingDelayDefault)
			{
				//Resetting to default value.
				pendingDelay = pendingDelayDefault;
			}
			DateTime now = DateTime.UtcNow;
			return now.AddMilliseconds(-1 * pendingDelay);
		}

		private DateTime GetSendingDelay()
		{
			const int sendingDelayDefault = 30 * 60 * 1000;

			int sendingDelay = this.ProviderConfiguration.ConfigNodeReader.GetCheckedIntValue("SendingStatusDelay", sendingDelayDefault, 0, Int32.MaxValue, true);
			DateTime now = DateTime.UtcNow;
			return now.AddMilliseconds(-1 * sendingDelay);
		}

		/// <summary>
		/// Cached smtpClient object for this provider
		/// </summary>
		private SmtpClient configuredSmtpClient;

		private SmtpClient ConfiguredSmtpClient
		{
			get
			{
				if (this.configuredSmtpClient == null)
				{
					// Get the SMTP configurations
					SmtpConfiguration smtpConfig = this.RetrieveSmtpConfiguration();

					// Create an SMTP client
					this.configuredSmtpClient = new SmtpClient()
					{
						Host = smtpConfig.ServerName,
						Port = smtpConfig.ServerPort,
						Timeout = smtpConfig.ConnectionTimeout,
						UseDefaultCredentials = smtpConfig.UseDefaultCredentials,
						EnableSsl = smtpConfig.UseSsl,
						DeliveryMethod = smtpConfig.DeliverUsing
					};

					if (!this.configuredSmtpClient.UseDefaultCredentials)
					{
						this.configuredSmtpClient.Credentials = smtpConfig.Credentials;
					}
				}

				return this.configuredSmtpClient;
			}
		}

		private BatchEmails _emails;
		private DateTime _delayedEmailsRetrieved;
	}

	/// <summary>
	/// Class to encapsulate the retrieved emails.
	/// </summary>
	internal class BatchEmails
	{
		/// <summary>
		/// List of Emails to be sent.
		/// </summary>
		private List<Entity> emails;

		/// <summary>
		/// Indicates if the retrieved email activity has associated attachments.
		/// </summary>
		private List<bool> hasAttachments;

		/// <summary>
		/// Ctor
		/// </summary>
		public BatchEmails()
		{
			this.index = -1;
			this.emails = new List<Entity>(15);
			this.hasAttachments = new List<bool>(15);
		}

		/// <summary>
		/// Add email/hasAttachment info.
		/// </summary>
		/// <param name="emailCollection">emails to add</param>
		/// <param name="emailHasAttachments">hasAttachments to add</param>
		public void AddRange(EntityCollection emailCollection, bool[] emailHasAttachments)
		{
			if (null != emailCollection && null != emailHasAttachments)
			{
				if (emailCollection.Entities.Count != emailHasAttachments.Length)
				{
					throw new CrmArgumentException("The size of both inputs should be same");
				}

				foreach (Entity email in emailCollection.Entities)
				{
					this.emails.Add(email);
				}

				this.hasAttachments.AddRange(emailHasAttachments);
			}
		}

		/// <summary>
		/// Retrieve the next email from the collection.
		/// </summary>
		/// <returns>Email.</returns>
		public Entity RetrieveNext()
		{
			if ((index + 1) < emails.Count)
			{
				return emails[++index];
			}
			return null;
		}

		/// <summary>
		/// Gets the current index.
		/// </summary>
		private int index;
		public int CurrentIndex
		{
			get { return (index); }
		}

		/// <summary>
		/// Returns the current has attachment value.
		/// </summary>
		public bool CurrentHasAttachment
		{
			get { return this.hasAttachments[this.CurrentIndex]; }
		}

	}
}
